package net.posick.ws.xml;

import java.io.IOException;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Stack;

import net.posick.tree.AbstractTree;
import net.posick.ws.soap.SOAPAction;
import net.posick.ws.soap.SOAPBody;
import net.posick.ws.soap.SOAPEnvelope;
import net.posick.ws.soap.SOAPException;
import net.posick.ws.soap.SOAPFault;
import net.posick.ws.soap.SOAPHeader;

import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlPullParserFactory;

/**
 * The XmlElement represents an XML Element.
 * 
 * @author Steve Posick
 */
public class XmlElement extends AbstractTree<XmlElement, CharSequence>
{
    protected static final long serialVersionUID = 201403191141L;

//    protected static final String LOG_TAG = Constants.LOG_TAG + "." + XmlElement.class.getSimpleName();
    
    protected static transient XmlPullParserFactory factory;
    
    
    public static final String NAMESPACE_ATTRIBUTE_KEY = "xmlns";
    
    
    protected Name name;
    
    protected Map<Name, CharSequence> attributes = new LinkedHashMap<Name, CharSequence>();
    
    protected Map<String, Namespace> namespaces = new LinkedHashMap<String, Namespace>();
    
    protected CharSequence value;
    
    
    /**
     * Creates an empty XmlElement, with no name, value, attributes, or namespaces.
     */
    public XmlElement()
    {
        super();
    }
    
    
    /**
     * Creates a new XmlElement with the specified name.
     * 
     * @param name The name of the XmlElement
     */
    public XmlElement(Name name)
    {
        this();
        setName(name);
    }
    
    
    protected void copyFrom(XmlElement element)
    {
        synchronized (this.attributes)
        {
            this.attributes.clear();
            this.attributes.putAll(element.attributes);
        }
        this.name = element.name;
        synchronized (this.namespaces)
        {
            this.namespaces.putAll(element.namespaces);
        }
        this.value = element.value;
    }
    
    
    /* (non-Javadoc)
     * @see net.posick.tree.AbstractTree#getValue()
     */
    @Override
    public CharSequence getValue()
    {
        return value;
    }
    
    
    /* (non-Javadoc)
     * @see net.posick.tree.AbstractTree#setValue(java.lang.Object)
     */
    @Override
    public XmlElement setValue(CharSequence value)
    {
        this.value = value;
        return this;
    }
    
    
    /**
     * Gets the name of the XmlElement.
     * 
     * @return The name of the XmlElement
     */
    public Name getName()
    {
        return name;
    }
    
    
    /**
     * Sets the name of the XmlElement.
     * 
     * @param name the name of the XmlElement
     */
    public XmlElement setName(Name name)
    {
        maintainNamespaces(name);
        this.name = name;
        return this;
    }
    
    
    protected void maintainNamespaces(Name name)
    {
        Namespace ns = name.getNamespace();
        if (ns.hasPrefix())
        {
            String prefix = ns.getPrefix();
            Namespace cachedNS = this.namespaces.get(prefix);
            if ((cachedNS == null || !cachedNS.hasUri()) && ns.hasUri() && (cachedNS == null || !ns.getUri().equals(cachedNS.getUri())))
            {
                this.namespaces.put(prefix, name.getNamespace());
            }
            
            /*
            Namespace ns = getNamespace(prefix);
            if (ns == null)
            {
                if (uri == null || uri.length() == 0)
                {
                    name.setPrefix(null);
                    System.err.println("No Namespace URI specified for prefix \"" + prefix + "\", setting prefix to null!");
                } else
                {
                    setNamespace(new Namespace(prefix, uri));
                }
            } else
            {
                if (uri == null || uri.length() == 0)
                {
                    name.setNamespaceUri(ns.getNamespaceUri());
                } else if (!uri.equals(ns.getNamespaceUri()))
                {
                    setNamespace(new Namespace(prefix, uri));
                }
            }
            */
        }
    }


    /**
     * Sets the provided XmlAttributes within the XmlElement.
     * 
     * @param attributes The XmlAttributes to set
     */
    public XmlElement setAttributes(Map<Name, CharSequence> attributes)
    {
        Set<Name> names = attributes.keySet();
        for (Name name : names)
        {
            Namespace ns = name.getNamespace();
            if (ns.hasPrefix() && ns.hasUri())
            {
                maintainNamespaces(name);
            }
        }
        
        synchronized (attributes)
        {
            this.attributes.clear();
            this.attributes.putAll(attributes);
            return this;
        }
    }
    
    
    /**
     * Adds the provided XmlAttributes to the XmlElement.
     * 
     * @param attributes The XmlAttributes to add
     */
    public XmlElement addAttributes(Map<Name, CharSequence> attributes)
    throws XmlException
    {
        synchronized (attributes)
        {
            for (Map.Entry<Name, CharSequence> entry : attributes.entrySet())
            {
                Name key = entry.getKey();
                CharSequence value = entry.getValue();
                
                if (!this.attributes.containsKey(key))
                {
                    maintainNamespaces(name);
                    this.attributes.put(key, value);
                } else
                {
                    throw new XmlException("Attribute \"" + key + "\" already exists.");
                }
            }
            return this;
        }
    }
    
    
    /**
     * Adds a new attribute to the XmlElement.  Will NOT change an existing value.
     * 
     * @param name The name of the attribute
     * @param value The value of the attribute
     * 
     * @return true If the attribute is added, false if the attribute already exists.
     */
    public XmlElement addAttribute(Name name, CharSequence value)
    throws XmlException
    {
        synchronized (attributes)
        {
            maintainNamespaces(name);
            if (!attributes.containsKey(name))
            {
                attributes.put(name, value);
            } else
            {
                throw new XmlException("Attribute \"" + name + "\" already exists.");
            }
        }
        
        return this;
    }
    
    
    /**
     * Sets the specified attribute to the value, adding the attribute if it does not already exist.
     * 
     * @param name The attribute name
     * @param value The attribute value
     */
    public XmlElement setAttribute(Name name, CharSequence value)
    {
        synchronized (attributes)
        {
            maintainNamespaces(name);
            this.attributes.put(name, value);
        }
        
        return this;
    }
    
    
    /**
     * Return the XmlAttributes associated to the XmlElement.
     * 
     * @return The XmlAttributes associated to the XmlElement
     */
    public Map<Name, CharSequence> getAttributes()
    {
        synchronized (attributes)
        {
            return Collections.unmodifiableMap(attributes);
        }
    }
    
    
    /**
     * Gets the attribute with the specified name.
     * 
     * @param name
     * @return
     */
    public CharSequence getAttribute(Name name)
    {
        return attributes.get(name);
    }
    
    
    /**
     * Gets the namspace Uri for the specified prefix.  Will work its way to the root to find the 
     * namespace Uri.
     * 
     * @param prefix The nmaespace prefix
     * @return The Uri for the namespace
     */
    public Namespace getNamespace(String prefix)
    {
        XmlElement node = this;
        Namespace ns = null;
        ns = node.namespaces.get(prefix);
        if (ns == null && parent != null)
        {
            ns = parent.getNamespace(prefix);
        }
        
        return ns;
    }
  
    
    /**
     * Gets the XML namespaces from the XmlElement.
     * 
     * @return The XML namespaces from the XmlElement
     */
    public Set<Namespace> getNamespaces()
    {
        synchronized (namespaces)
        {
            return new LinkedHashSet<Namespace>(namespaces.values());
        }
    }
    
    
    /**
     * Sets the provided XmlNamespaces to the XmlElement.
     * 
     * @param nodes The XmlNamespaces to set
     * @throws XmlException 
     */
    public XmlElement setNamespaces(Set<Namespace> namespaces)
    {
        synchronized (this.namespaces)
        {
            this.namespaces.clear();
            
            for (Namespace namespace : namespaces)
            {
                setNamespace(namespace);
            }
        }
        
        return this;
    }
    
    
    /**
     * Adds the provided XmlNamespaces to the XmlElement.
     * 
     * @param nodes The XmlNamespaces to add
     * @throws XmlException 
     */
    public XmlElement setNamespace(Namespace namespace)
    {
        synchronized (this.namespaces)
        {
            String prefix = namespace.getPrefix();
            Namespace ns = getNamespace(prefix);
            
            if (ns == null)
            {
                this.namespaces.put(prefix, namespace);
            } else
            {
                String uri = ns.getUri();
                if (uri == null || uri.length() == 0 || !uri.equals(namespace.getUri()))
                {
                    this.namespaces.put(prefix, namespace);
                }
            }
        }
        
        return this;
    }
    
    
    /**
     * Adds the provided XmlNamespaces to the XmlElement.
     * 
     * @param nodes The XmlNamespaces to add
     */
    public XmlElement addNamespaces(Set<Namespace> namespaces)
    throws XmlException
    {
        synchronized (this.namespaces)
        {
            for (Namespace namespace : namespaces)
            {
                String prefix = namespace.getPrefix();
                if (!this.namespaces.containsKey(prefix))
                {
                    this.namespaces.put(prefix, namespace);
                } else if (!namespace.getUri().equals(this.namespaces.get(prefix)))
                {
                    throw new XmlException("Namespace \"" + namespace + "\" already exists.");
                }
            }
        }
        
        return this;
    }
    
    
    /**
     * Adds the provided XmlNamespaces to the XmlElement.
     * 
     * @param nodes The XmlNamespaces to add
     * @throws XmlException 
     */
    public XmlElement addNamespace(Namespace... namespaces)
    throws XmlException
    {
        return addNamespaces(new LinkedHashSet<Namespace>(Arrays.asList(namespaces)));
    }
    
    
    @Override
    public XmlElement add(XmlElement newNode, int mode)
    throws XmlException
    {
        return super.add(newNode, mode);
    }


    @Override
    public XmlElement addChild(XmlElement newNode)
    throws XmlException
    {
        return this.add(newNode, XmlElement.LASTCHILD);
    }


    /**
     * Gets the of the children that have an XML Element name matching the specified name.
     * 
     * @param name The name of the XML Element
     * @return The children that have an XML Element name matching the specified name
     */
    public List<XmlElement> getChildren(Name name)
    {
        List<XmlElement> results = new ArrayList<XmlElement>();
        
        List<XmlElement> elements = Collections.unmodifiableList(getChildren());
        for (XmlElement element : elements)
        {
            if (name.equals(element.getName()))
            {
                results.add(element);
            }
        }
        
        return results.size() == 0 ? null : results;
    }
    
    
    /**
     * Gets the of the children starting at the start position up to the element before the end position.
     * 
     * @param start The start position of the first element to include.
     * @param end The end position, one position beyond the last element to be returned.
     * @return The children starting at the start position up to the element before the end position
     */
    public List<XmlElement> getChildren(int start, int end)
    {
        return Collections.unmodifiableList(getChildren()).subList(start, end);
    }
    
    
    /**
     * Clears the XmlElement, removing all children, attributes, namespaces, and values, but does not 
     * change the XmlElement's placement within the tree or its name.
     */
    public synchronized XmlElement clear()
    {
        synchronized (attributes)
        {
            synchronized (namespaces)
            {
                super.child = null;
                this.value = null;
                this.attributes.clear();
                this.namespaces.clear();
                return this;
            }
        }
    }
    
    
    /* (non-Javadoc)
     * @see net.posick.tree.AbstractTree#toString()
     */
    public String toString()
    {
        final StringBuilder builder = new StringBuilder();
        synchronized (attributes)
        {
            synchronized (namespaces)
            {
                traverse(new XmlBuilder(builder));
            }
        }
        return builder.toString();
    }
    
    
    /**
     * Loads the XML from the CharSequence.
     * 
     * @param xml The XML
     * @return The XML represented in Object form
     * @throws Exception
     */
    public static XmlElement parse(CharSequence xml)
    throws XmlException
    {
        try
        {
            // Parse the XML String into an XmlElement object representations
            XmlPullParser parser = xmlPullParserFactory().newPullParser();
            parser.setInput(new StringReader(xml.toString()));
            
            // Store namespaces URIs by prefix.
            Stack<XmlElement> stack = new Stack<XmlElement>();
            XmlElement rootElement = null;
            XmlElement currentElement = null;
            XmlElement parentElement = null;
            
            int eventType = parser.getEventType();
            while (eventType != XmlPullParser.END_DOCUMENT)
            {
                switch (parser.getEventType())
                {
                    case XmlPullParser.START_DOCUMENT:
                        break;
                    case XmlPullParser.START_TAG:
                        // Build XmlNamespaces
                        Set<Namespace> namespaces  = new LinkedHashSet<Namespace>();
                        Map<String, String> namespaceMap  = new LinkedHashMap<String, String>();
                        int startNS = parser.getNamespaceCount(parser.getDepth() - 1);
                        int endNS = parser.getNamespaceCount(parser.getDepth());
                        if (endNS - startNS > 0)
                        {
                            namespaces = new LinkedHashSet<Namespace>();
                            for (int index = startNS; index < endNS; index++)
                            {
                                String prefix = parser.getNamespacePrefix(index);
                                String uri = parser.getNamespaceUri(index);
                                if (prefix != null)
                                {
                                    namespaceMap.put(prefix, uri);
                                    Namespace namespace = new Namespace(prefix, uri);
                                    namespaces.add(namespace);
                                }
                            }
                        }
                        
                        // Build XmlAttributes
                        Map<Name, CharSequence> attributes = null;
                        int attributeCount = parser.getAttributeCount();
                        if (attributeCount > 0)
                        {
                            attributes = new LinkedHashMap<Name, CharSequence>();
                            for (int index = 0; index < attributeCount; index++)
                            {
                                String attrNamespaceUri = parser.getAttributeNamespace(index);
                                String attrPrefix = parser.getAttributePrefix(index);
                                String attrName = parser.getAttributeName(index);
                                String attrValue = parser.getAttributeValue(index);
                                
                                Name name = new Name(new Namespace(attrPrefix, attrNamespaceUri), attrName);
                                attributes.put(name, attrValue);
                            }
                        }
                        
                        // Build XmlElement
                        currentElement = new XmlElement();
                        if (!stack.isEmpty())
                        {
                            parentElement = stack.peek();
                        }
                        
                        String prefix = parser.getPrefix();
                        String nsUri = parser.getNamespace();
                        if (nsUri == null || nsUri.length() == 0)
                        {
                            nsUri = namespaceMap.get(prefix);
                            if (parentElement != null)
                            {
                                Namespace ns = currentElement.getNamespace(prefix);
                                nsUri = ns != null ? ns.getUri() : null;
                            }
                        }
                        currentElement.setName(new Name(new Namespace(prefix, nsUri), parser.getName()));
                        
                        if (attributes != null)
                        {
                            currentElement.setAttributes(attributes);
                        }
                        
                        if (namespaces != null)
                        {
                            currentElement.setNamespaces(namespaces);
                        }
                        if (rootElement == null)
                        {
                            rootElement = currentElement;
                        }
                        stack.push(currentElement);
                        
                        attributes = null;
                        namespaces = null;
                        namespaceMap = null;
                        break;
                    case XmlPullParser.TEXT:
                        // Set the text for the current XmlElement.
                        currentElement = stack.peek();
                        if (currentElement != null)
                        {
                            CharSequence value = currentElement.getValue();
                            String text = parser.getText();
                            currentElement.setValue((value == null ? "" : value.toString()) + (text == null ? "" : text));
                        }
                        break;
                    case XmlPullParser.END_TAG:
                        // Add XmlElement to previous node.
                        //  if depth > previous depth, 
                        //      Add as child.
                        //  if depth == parent depth, 
                        //      Add as sibling.
                        //  if depth < parent depth,
                        //      Add as parent.
                        currentElement = stack.pop();
                        if (!stack.isEmpty())
                        {
                            parentElement = stack.peek();
                        }
                        
                        if (parentElement != null)
                        {
                            parentElement.add(currentElement, XmlElement.LASTCHILD);
                        }
                        parentElement = null;
                        break;
                }
                
                eventType = parser.next();
            }
            
            // Clear parser
            parser.setInput(null);
            
            return rootElement;
        } catch (XmlPullParserException e)
        {
            throw XmlException.wrap(e);
        } catch (IOException e)
        {
            throw XmlException.wrap(e);
        }
    }
    
    
    /**
     * Returns a pre-configured instance of a XmlPullParserFactory.
     * 
     * @return Returns a pre-configured instance of a XmlPullParserFactory
     * @throws XmlPullParserException
     */
    protected static synchronized XmlPullParserFactory xmlPullParserFactory()
    throws XmlPullParserException
    {
        if (factory == null)
        {
            factory = XmlPullParserFactory.newInstance();
            factory.setNamespaceAware(true);
            factory.setValidating(false);
            factory.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
//            factory.setFeature(XmlPullParser.FEATURE_REPORT_NAMESPACE_ATTRIBUTES, true);
        }
        return factory;
    }
    
    
    public static void main(String... args)
    {
        XmlElement response = new XmlElement(new Name(new Namespace("device", "http://www.smpte-ra.org/schemas/st2071/2014/device"), "getUDNResponse"));
        response.addChild(new XmlElement(new Name(new Namespace("identity", "http://www.smpte-ra.org/schemas/st2071/2014/identity"), "UDN"))
                                .setValue("urn:smpte:udn:org.smpte:test"));
        
        String soapAction = "test:Action";
        SOAPHeader header = new SOAPHeader();

        if (soapAction != null && soapAction.length() > 0)
        {
            header.addChild(new SOAPAction(soapAction));
        }
        
        SOAPBody body = null;
        try
        {
             body = new SOAPBody(response);
        } catch (SOAPException e)
        {
            body = new SOAPBody(new SOAPFault(e));
        }
        SOAPEnvelope envelope = new SOAPEnvelope(header, body);
        
        System.out.println(envelope.toString()); 
    }
}
